/*
 * %W% %E%
 *
 * Copyright (c) 2006, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package javax.swing.text.rtf;

import java.lang.*;
import java.util.*;
import java.io.*;
import java.awt.Font;
import java.awt.Color;

import javax.swing.text.*;

/**
 * Takes a sequence of RTF tokens and text and appends the text
 * described by the RTF to a <code>StyledDocument</code> (the <em>target</em>).
 * The RTF is lexed
 * from the character stream by the <code>RTFParser</code> which is this class's
 * superclass.
 *
 * This class is an indirect subclass of OutputStream. It must be closed
 * in order to guarantee that all of the text has been sent to
 * the text acceptor.
 *
 *   @see RTFParser
 *   @see java.io.OutputStream
 */
class RTFReader extends RTFParser
{
  /** The object to which the parsed text is sent. */
  StyledDocument target;

  /** Miscellaneous information about the parser's state. This
   *  dictionary is saved and restored when an RTF group begins
   *  or ends. */
  Dictionary parserState;   /* Current parser state */
  /** This is the "dst" item from parserState. rtfDestination
   *  is the current rtf destination. It is cached in an instance
   *  variable for speed. */
  Destination rtfDestination;
  /** This holds the current document attributes. */
  MutableAttributeSet documentAttributes;

  /** This Dictionary maps Integer font numbers to String font names. */
  Dictionary fontTable;
  /** This array maps color indices to Color objects. */
  Color[] colorTable;
  /** This array maps character style numbers to Style objects. */
  Style[] characterStyles;
  /** This array maps paragraph style numbers to Style objects. */
  Style[] paragraphStyles;
  /** This array maps section style numbers to Style objects. */
  Style[] sectionStyles;

  /** This is the RTF version number, extracted from the \rtf keyword.
   *  The version information is currently not used. */
  int rtfversion;

  /** <code>true</code> to indicate that if the next keyword is unknown,
   *  the containing group should be ignored. */
  boolean ignoreGroupIfUnknownKeyword;

  /** The parameter of the most recently parsed \\ucN keyword,
   *  used for skipping alternative representations after a
   *  Unicode character. */
  int skippingCharacters;  

  static private Dictionary straightforwardAttributes;
  static {
      straightforwardAttributes = RTFAttributes.attributesByKeyword();
  }

  private MockAttributeSet mockery;

  /* this should be final, but there's a bug in javac... */
  /** textKeywords maps RTF keywords to single-character strings,
   *  for those keywords which simply insert some text. */
  static Dictionary textKeywords = null;
  static {
      textKeywords = new Hashtable();
      textKeywords.put("\\",         "\\");
      textKeywords.put("{",          "{");
      textKeywords.put("}",          "}");
      textKeywords.put(" ",          "\u00A0");  /* not in the spec... */
      textKeywords.put("~",          "\u00A0");  /* nonbreaking space */
      textKeywords.put("_",          "\u2011");  /* nonbreaking hyphen */
      textKeywords.put("bullet",     "\u2022");
      textKeywords.put("emdash",     "\u2014");
      textKeywords.put("emspace",    "\u2003");
      textKeywords.put("endash",     "\u2013");
      textKeywords.put("enspace",    "\u2002");
      textKeywords.put("ldblquote",  "\u201C");
      textKeywords.put("lquote",     "\u2018");
      textKeywords.put("ltrmark",    "\u200E");
      textKeywords.put("rdblquote",  "\u201D");
      textKeywords.put("rquote",     "\u2019");
      textKeywords.put("rtlmark",    "\u200F");
      textKeywords.put("tab",        "\u0009");
      textKeywords.put("zwj",        "\u200D");
      textKeywords.put("zwnj",       "\u200C");
      
      /* There is no Unicode equivalent to an optional hyphen, as far as
	 I can tell. */
      textKeywords.put("-",          "\u2027");  /* TODO: optional hyphen */
  }

  /* some entries in parserState */
  static final String TabAlignmentKey = "tab_alignment";
  static final String TabLeaderKey = "tab_leader";
	
  static Dictionary characterSets;
  static boolean useNeXTForAnsi = false;
  static {
      characterSets = new Hashtable();
  }

/* TODO: per-font font encodings ( \fcharset control word ) ? */

/**
 * Creates a new RTFReader instance. Text will be sent to
 * the specified TextAcceptor.
 *
 * @param destination The TextAcceptor which is to receive the text.
 */
public RTFReader(StyledDocument destination)
{
    int i;

    target = destination;
    parserState = new Hashtable();
    fontTable = new Hashtable();

    rtfversion = -1;

    mockery = new MockAttributeSet();
    documentAttributes = new SimpleAttributeSet();
}

/** Called when the RTFParser encounters a bin keyword in the
 *  RTF stream.
 *
 *  @see RTFParser
 */
public void handleBinaryBlob(byte[] data)
{
    if (skippingCharacters > 0) {
	/* a blob only counts as one character for skipping purposes */
	skippingCharacters --;
	return;
    }

    /* someday, someone will want to do something with blobs */
}


/**
 * Handles any pure text (containing no control characters) in the input
 * stream. Called by the superclass. */
public void handleText(String text)
{
    if (skippingCharacters > 0) {
	if (skippingCharacters >= text.length()) {
	    skippingCharacters -= text.length();
	    return;
	} else {
	    text = text.substring(skippingCharacters);
	    skippingCharacters = 0;
	}
    }

    if (rtfDestination != null) {
	rtfDestination.handleText(text);
	return;
    }

    warning("Text with no destination. oops.");
}

/** The default color for text which has no specified color. */
Color defaultColor()
{
    return Color.black;
}

/** Called by the superclass when a new RTF group is begun.
 *  This implementation saves the current <code>parserState</code>, and gives
 *  the current destination a chance to save its own state.
 * @see RTFParser#begingroup
 */
public void begingroup()
{
    if (skippingCharacters > 0) {
	/* TODO this indicates an error in the RTF. Log it? */
	skippingCharacters = 0;
    }

    /* we do this little dance to avoid cloning the entire state stack and
       immediately throwing it away. */
    Object oldSaveState = parserState.get("_savedState");
    if (oldSaveState != null)
	parserState.remove("_savedState");
    Dictionary saveState = (Dictionary)((Hashtable)parserState).clone();
    if (oldSaveState != null)
	saveState.put("_savedState", oldSaveState);
    parserState.put("_savedState", saveState);

    if (rtfDestination != null)
	rtfDestination.begingroup();
}

/** Called by the superclass when the current RTF group is closed.
 *  This restores the parserState saved by <code>begingroup()</code>
 *  as well as invoking the endgroup method of the current
 *  destination.
 * @see RTFParser#endgroup
 */
public void endgroup()
{
    if (skippingCharacters > 0) {
	/* NB this indicates an error in the RTF. Log it? */
	skippingCharacters = 0;
    }

    Dictionary restoredState = (Dictionary)parserState.get("_savedState");
    Destination restoredDestination = (Destination)restoredState.get("dst");
    if (restoredDestination != rtfDestination) {
	rtfDestination.close(); /* allow the destination to clean up */
	rtfDestination = restoredDestination;
    }
    Dictionary oldParserState = parserState;
    parserState = restoredState;
    if (rtfDestination != null)
	rtfDestination.endgroup(oldParserState);
}

protected void setRTFDestination(Destination newDestination)
{
    /* Check that setting the destination won't close the
       current destination (should never happen) */
    Dictionary previousState = (Dictionary)parserState.get("_savedState");
    if (previousState != null) {
	if (rtfDestination != previousState.get("dst")) {
	    warning("Warning, RTF destination overridden, invalid RTF.");
	    rtfDestination.close();
	}
    }
    rtfDestination = newDestination;
    parserState.put("dst", rtfDestination);
}

/** Called by the user when there is no more input (<i>i.e.</i>,
 * at the end of the RTF file.)
 *
 * @see OutputStream#close
 */
public void close()
    throws IOException
{
    Enumeration docProps = documentAttributes.getAttributeNames();
    while(docProps.hasMoreElements()) {
        Object propName = docProps.nextElement();
	target.putProperty(propName,
			   documentAttributes.getAttribute((String)propName));
    }

    /* RTFParser should have ensured that all our groups are closed */

    warning("RTF filter done.");

    super.close();
}

/**
 * Handles a parameterless RTF keyword. This is called by the superclass
 * (RTFParser) when a keyword is found in the input stream.
 *
 * @returns <code>true</code> if the keyword is recognized and handled;
 *          <code>false</code> otherwise
 * @see RTFParser#handleKeyword
 */
public boolean handleKeyword(String keyword)
{
    Object item;
    boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword;

    if (skippingCharacters > 0) {
	skippingCharacters --;
	return true;
    }

    ignoreGroupIfUnknownKeyword = false;
    
    if ((item = textKeywords.get(keyword)) != null) {
	handleText((String)item);
	return true;
    }
    
    if (keyword.equals("fonttbl")) {
	setRTFDestination(new FonttblDestination());
        return true;
    }

    if (keyword.equals("colortbl")) {
	setRTFDestination(new ColortblDestination());
        return true;
    }

    if (keyword.equals("stylesheet")) {
	setRTFDestination(new StylesheetDestination());
        return true;
    }

    if (keyword.equals("info")) {
	setRTFDestination(new InfoDestination());
	return false; 
    }

    if (keyword.equals("mac")) {
	setCharacterSet("mac");
        return true;
    }

    if (keyword.equals("ansi")) {
	if (useNeXTForAnsi)
	    setCharacterSet("NeXT");
	else
	    setCharacterSet("ansi");
        return true;
    }

    if (keyword.equals("next")) {
	setCharacterSet("NeXT");
	return true;
    }

    if (keyword.equals("pc")) {
	setCharacterSet("cpg437"); /* IBM Code Page 437 */
        return true;
    }

    if (keyword.equals("pca")) {
	setCharacterSet("cpg850"); /* IBM Code Page 850 */
        return true;
    }

    if (keyword.equals("*")) {
        ignoreGroupIfUnknownKeyword = true;
        return true;
    }

    if (rtfDestination != null) {
	if(rtfDestination.handleKeyword(keyword))
	    return true;
    }

    /* this point is reached only if the keyword is unrecognized */

    /* other destinations we don't understand and therefore ignore */
    if (keyword.equals("aftncn") ||
	keyword.equals("aftnsep") ||
	keyword.equals("aftnsepc") ||
	keyword.equals("annotation") ||
	keyword.equals("atnauthor") ||
	keyword.equals("atnicn") ||
	keyword.equals("atnid") ||
	keyword.equals("atnref") ||
	keyword.equals("atntime") ||
	keyword.equals("atrfend") ||
	keyword.equals("atrfstart") ||
	keyword.equals("bkmkend") ||
	keyword.equals("bkmkstart") ||
	keyword.equals("datafield") ||
	keyword.equals("do") ||
	keyword.equals("dptxbxtext") ||
	keyword.equals("falt") ||
	keyword.equals("field") ||
	keyword.equals("file") ||
	keyword.equals("filetbl") ||
	keyword.equals("fname") ||
	keyword.equals("fontemb") ||
	keyword.equals("fontfile") ||
	keyword.equals("footer") ||
	keyword.equals("footerf") ||
	keyword.equals("footerl") ||
	keyword.equals("footerr") ||
	keyword.equals("footnote") ||
	keyword.equals("ftncn") ||
	keyword.equals("ftnsep") ||
	keyword.equals("ftnsepc") ||
	keyword.equals("header") ||
	keyword.equals("headerf") ||
	keyword.equals("headerl") ||
	keyword.equals("headerr") ||
	keyword.equals("keycode") ||
	keyword.equals("nextfile") ||
	keyword.equals("object") ||
	keyword.equals("pict") ||
	keyword.equals("pn") ||
	keyword.equals("pnseclvl") ||
	keyword.equals("pntxtb") ||
	keyword.equals("pntxta") ||
	keyword.equals("revtbl") ||
	keyword.equals("rxe") ||
	keyword.equals("tc") ||
	keyword.equals("template") ||
	keyword.equals("txe") ||
	keyword.equals("xe")) {
	ignoreGroupIfUnknownKeywordSave = true;
    }

    if (ignoreGroupIfUnknownKeywordSave) {
	setRTFDestination(new DiscardingDestination());
    }

    return false;
}

/**
 * Handles an RTF keyword and its integer parameter. 
 * This is called by the superclass
 * (RTFParser) when a keyword is found in the input stream.
 *
 * @returns <code>true</code> if the keyword is recognized and handled;
 *          <code>false</code> otherwise
 * @see RTFParser#handleKeyword
 */
public boolean handleKeyword(String keyword, int parameter)
{
    boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword;

    if (skippingCharacters > 0) {
	skippingCharacters --;
	return true;
    }

    ignoreGroupIfUnknownKeyword = false;

    if (keyword.equals("uc")) {
	/* count of characters to skip after a unicode character */
	parserState.put("UnicodeSkip", Integer.valueOf(parameter));
	return true;
    }
    if (keyword.equals("u")) {
	if (parameter < 0)
	    parameter = parameter + 65536;
	handleText((char)parameter);
	Number skip = (Number)(parserState.get("UnicodeSkip"));
	if (skip != null) {
	    skippingCharacters = skip.intValue();
	} else {
	    skippingCharacters = 1;
	}
	return true;
    }

    if (keyword.equals("rtf")) {
        rtfversion = parameter;
	setRTFDestination(new DocumentDestination());
        return true;
    }

    if (keyword.startsWith("NeXT") ||
	keyword.equals("private"))
	ignoreGroupIfUnknownKeywordSave = true;

    if (rtfDestination != null) {
	if(rtfDestination.handleKeyword(keyword, parameter))
	    return true;
    }

    /* this point is reached only if the keyword is unrecognized */

    if (ignoreGroupIfUnknownKeywordSave) {
	setRTFDestination(new DiscardingDestination());
    }

    return false;
}

private void setTargetAttribute(String name, Object value)
{
//    target.changeAttributes(new LFDictionary(LFArray.arrayWithObject(value), LFArray.arrayWithObject(name)));
}

/**
 * setCharacterSet sets the current translation table to correspond with
 * the named character set. The character set is loaded if necessary.
 *
 * @see AbstractFilter
 */
public void setCharacterSet(String name)
{
    Object set;

    try {
        set = getCharacterSet(name);
    } catch (Exception e) {
	warning("Exception loading RTF character set \"" + name + "\": " + e);
	set = null;
    }

    if (set != null) {
	translationTable = (char[])set;
    } else {
	warning("Unknown RTF character set \"" + name + "\"");
	if (!name.equals("ansi")) {
	    try {
		translationTable = (char[])getCharacterSet("ansi");
	    } catch (IOException e) {
		throw new InternalError("RTFReader: Unable to find character set resources (" + e + ")");
	    }
	}
    }

    setTargetAttribute(Constants.RTFCharacterSet, name);
}

/** Adds a character set to the RTFReader's list
 *  of known character sets */
public static void 
defineCharacterSet(String name, char[] table)
{
    if (table.length < 256)
	throw new IllegalArgumentException("Translation table must have 256 entries.");
    characterSets.put(name, table);
}

/** Looks up a named character set. A character set is a 256-entry
 *  array of characters, mapping unsigned byte values to their Unicode
 *  equivalents. The character set is loaded if necessary.
 *
 *  @returns the character set
 */
public static Object
getCharacterSet(final String name)
    throws IOException
{
    char[] set;

    set = (char [])characterSets.get(name);
    if (set == null) {
      InputStream charsetStream;
      charsetStream = (InputStream)java.security.AccessController.
	              doPrivileged(new java.security.PrivilegedAction() {
	  public Object run() {
	      return RTFReader.class.getResourceAsStream
		                     ("charsets/" + name + ".txt");
	  }
      });
      set = readCharset(charsetStream);
      defineCharacterSet(name, set);
    }
    return set;
}

/** Parses a character set from an InputStream. The character set
 * must contain 256 decimal integers, separated by whitespace, with
 * no punctuation. B- and C- style comments are allowed.
 *
 * @returns the newly read character set
 */
static char[] readCharset(InputStream strm)
     throws IOException
{
    char[] values = new char[256];
    int i;
    StreamTokenizer in = new StreamTokenizer(new BufferedReader(
            new InputStreamReader(strm, "ISO-8859-1")));

    in.eolIsSignificant(false);
    in.commentChar('#');
    in.slashSlashComments(true);
    in.slashStarComments(true);

    i = 0;
    while (i < 256) {
	int ttype;
	try {
	    ttype = in.nextToken();
	} catch (Exception e) {
	    throw new IOException("Unable to read from character set file (" + e + ")");
	}
	if (ttype != in.TT_NUMBER) {
//	    System.out.println("Bad token: type=" + ttype + " tok=" + in.sval);
	    throw new IOException("Unexpected token in character set file");
//	    continue;
	}
	values[i] = (char)(in.nval);
	i++;
    }

    return values;
}

static char[] readCharset(java.net.URL href)
     throws IOException
{
    return readCharset(href.openStream());
}

/** An interface (could be an entirely abstract class) describing
 *  a destination. The RTF reader always has a current destination
 *  which is where text is sent. 
 *
 *  @see RTFReader
 */
interface Destination {
    void handleBinaryBlob(byte[] data);
    void handleText(String text);
    boolean handleKeyword(String keyword);
    boolean handleKeyword(String keyword, int parameter);

    void begingroup();
    void endgroup(Dictionary oldState);

    void close();
}

/** This data-sink class is used to implement ignored destinations
 *  (e.g. {\*\blegga blah blah blah} )
 *  It accepts all keywords and text but does nothing with them. */
class DiscardingDestination implements Destination
{
    public void handleBinaryBlob(byte[] data)
    {
	/* Discard binary blobs. */
    }

    public void handleText(String text)
    {
	/* Discard text. */
    }

    public boolean handleKeyword(String text)
    {
	/* Accept and discard keywords. */
	return true;
    }

    public boolean handleKeyword(String text, int parameter)
    {
	/* Accept and discard parameterized keywords. */
	return true;
    }

    public void begingroup()
    {
	/* Ignore groups --- the RTFReader will keep track of the
	   current group level as necessary */
    }

    public void endgroup(Dictionary oldState)
    {
	/* Ignore groups */
    }

    public void close()
    {
	/* No end-of-destination cleanup needed */
    }
}

/** Reads the fonttbl group, inserting fonts into the RTFReader's
 *  fontTable dictionary. */
class FonttblDestination implements Destination
{
    int nextFontNumber;
    Object fontNumberKey = null;
    String nextFontFamily;
    
    public void handleBinaryBlob(byte[] data)
    { /* Discard binary blobs. */ }

    public void handleText(String text)
    {
        int semicolon = text.indexOf(';');
        String fontName;

        if (semicolon > -1)
            fontName = text.substring(0, semicolon);
        else
            fontName = text;
        
        
        /* TODO: do something with the font family. */

        if (nextFontNumber == -1 
            && fontNumberKey != null) {
            //font name might be broken across multiple calls
            fontName = fontTable.get(fontNumberKey) + fontName;
        } else {
            fontNumberKey = Integer.valueOf(nextFontNumber);
        }
        fontTable.put(fontNumberKey, fontName);

	nextFontNumber = -1;
	nextFontFamily = null;
        return;
    }

    public boolean handleKeyword(String keyword)
    {
	if (keyword.charAt(0) == 'f') {
	    nextFontFamily = keyword.substring(1);
	    return true;
	}
	
	return false;
    }

    public boolean handleKeyword(String keyword, int parameter)
    {
	if (keyword.equals("f")) {
	    nextFontNumber = parameter;
	    return true;
	}

	return false;
    }

    /* Groups are irrelevant. */
    public void begingroup() {}
    public void endgroup(Dictionary oldState) {}

    /* currently, the only thing we do when the font table ends is
       dump its contents to the debugging log. */
    public void close()
    {
        Enumeration nums = fontTable.keys();
        warning("Done reading font table.");
        while(nums.hasMoreElements()) {
            Integer num = (Integer)nums.nextElement();
            warning("Number " + num + ": " + fontTable.get(num));
        }
    }
}

/** Reads the colortbl group. Upon end-of-group, the RTFReader's
 *  color table is set to an array containing the read colors. */
class ColortblDestination implements Destination
{
    int red, green, blue;
    Vector proTemTable;

    public ColortblDestination()
    {
	red = 0;
	green = 0;
	blue = 0;
	proTemTable = new Vector();
    }

    public void handleText(String text)
    {
        int index = 0;

        for (index = 0; index < text.length(); index ++) {
            if (text.charAt(index) == ';') {
                Color newColor;
		newColor = new Color(red, green, blue);
		proTemTable.addElement(newColor);
            }
        }
    }

    public void close()
    {
	int count = proTemTable.size();
        warning("Done reading color table, " + count + " entries.");
	colorTable = new Color[count];
	proTemTable.copyInto(colorTable);
    }

    public boolean handleKeyword(String keyword, int parameter)
    {
        if (keyword.equals("red"))
	    red = parameter;
	else if (keyword.equals("green"))
	    green = parameter;
	else if (keyword.equals("blue"))
	    blue = parameter;
	else
	    return false;
	
	return true;
    }

    /* Colortbls don't understand any parameterless keywords */
    public boolean handleKeyword(String keyword) { return false; }

    /* Groups are irrelevant. */
    public void begingroup() {}
    public void endgroup(Dictionary oldState) {}

    /* Shouldn't see any binary blobs ... */
    public void handleBinaryBlob(byte[] data) {}
}

/** Handles the stylesheet keyword. Styles are read and sorted
 *  into the three style arrays in the RTFReader. */
class StylesheetDestination
    extends DiscardingDestination
    implements Destination
{
    Dictionary definedStyles;

    public StylesheetDestination()
    {
	definedStyles = new Hashtable();
    }

    public void begingroup()
    {
	setRTFDestination(new StyleDefiningDestination());
    }

    public void close() 
    {
        Vector chrStyles, pgfStyles, secStyles;
	chrStyles = new Vector();
	pgfStyles = new Vector();
	secStyles = new Vector();
	Enumeration styles = definedStyles.elements();
	while(styles.hasMoreElements()) {
	    StyleDefiningDestination style;
	    Style defined;
	    style = (StyleDefiningDestination)styles.nextElement();
	    defined = style.realize();
	    warning("Style "+style.number+" ("+style.styleName+"): "+defined);
	    String stype = (String)defined.getAttribute(Constants.StyleType);
	    Vector toSet;
	    if (stype.equals(Constants.STSection)) {
	        toSet = secStyles;
	    } else if (stype.equals(Constants.STCharacter)) {
	        toSet = chrStyles;
	    } else {
	        toSet = pgfStyles;
	    }
	    if (toSet.size() <= style.number)
	        toSet.setSize(style.number + 1);
	    toSet.setElementAt(defined, style.number);
	}
	if (!(chrStyles.isEmpty())) {
	    Style[] styleArray = new Style[chrStyles.size()];
	    chrStyles.copyInto(styleArray);
	    characterStyles = styleArray;
	}
	if (!(pgfStyles.isEmpty())) {
	    Style[] styleArray = new Style[pgfStyles.size()];
	    pgfStyles.copyInto(styleArray);
	    paragraphStyles = styleArray;
	}
	if (!(secStyles.isEmpty())) {
	    Style[] styleArray = new Style[secStyles.size()];
	    secStyles.copyInto(styleArray);
	    sectionStyles = styleArray;
	}

/* (old debugging code)
	int i, m;
	if (characterStyles != null) {
	  m = characterStyles.length;
	  for(i=0;i<m;i++) 
	    warnings.println("chrStyle["+i+"]="+characterStyles[i]);
	} else warnings.println("No character styles.");
	if (paragraphStyles != null) {
	  m = paragraphStyles.length;
	  for(i=0;i<m;i++) 
	    warnings.println("pgfStyle["+i+"]="+paragraphStyles[i]);
	} else warnings.println("No paragraph styles.");
	if (sectionStyles != null) {
	  m = characterStyles.length;
	  for(i=0;i<m;i++) 
	    warnings.println("secStyle["+i+"]="+sectionStyles[i]);
	} else warnings.println("No section styles.");
*/
    }

    /** This subclass handles an individual style */
    class StyleDefiningDestination
	extends AttributeTrackingDestination
	implements Destination
    {
	final int STYLENUMBER_NONE = 222; 
	boolean additive;
	boolean characterStyle;
	boolean sectionStyle;
	public String styleName;
	public int number;
	int basedOn;
	int nextStyle;
	boolean hidden;

	Style realizedStyle;

	public StyleDefiningDestination()
	{
	    additive = false;
	    characterStyle = false;
	    sectionStyle = false;
	    styleName = null;
	    number = 0;
	    basedOn = STYLENUMBER_NONE;
	    nextStyle = STYLENUMBER_NONE;
	    hidden = false;
	}
	
	public void handleText(String text)
	{
	    if (styleName != null)
		styleName = styleName + text;
	    else
		styleName = text;
	}

	public void close() {
            int semicolon = (styleName == null) ? 0 : styleName.indexOf(';');
	    if (semicolon > 0)
		styleName = styleName.substring(0, semicolon);
	    definedStyles.put(Integer.valueOf(number), this);
	    super.close();
	}

	public boolean handleKeyword(String keyword)
	{
	    if (keyword.equals("additive")) {
		additive = true;
		return true;
	    }
	    if (keyword.equals("shidden")) {
		hidden = true;
		return true;
	    }
	    return super.handleKeyword(keyword);
	}

	public boolean handleKeyword(String keyword, int parameter)
	{
	    if (keyword.equals("s")) {
		characterStyle = false;
		sectionStyle = false;
		number = parameter;
	    } else if (keyword.equals("cs")) {
		characterStyle = true;
		sectionStyle = false;
		number = parameter;
	    } else if (keyword.equals("ds")) {
		characterStyle = false;
		sectionStyle = true;
		number = parameter;
	    } else if (keyword.equals("sbasedon")) {
		basedOn = parameter;
	    } else if (keyword.equals("snext")) {
		nextStyle = parameter;
	    } else {
		return super.handleKeyword(keyword, parameter);
	    }
	    return true;
	}

	public Style realize()
	{
	    Style basis = null;
	    Style next = null;
	    
	    if (realizedStyle != null)
		return realizedStyle;

	    if (basedOn != STYLENUMBER_NONE) {
		StyleDefiningDestination styleDest;
		styleDest = (StyleDefiningDestination)definedStyles.get(Integer.valueOf(basedOn));
		if (styleDest != null && styleDest != this) {
		    basis = styleDest.realize();
		}
	    }

	    /* NB: Swing StyleContext doesn't allow distinct styles with
	       the same name; RTF apparently does. This may confuse the
	       user. */
	    realizedStyle = target.addStyle(styleName, basis);

	    if (characterStyle) {
		realizedStyle.addAttributes(currentTextAttributes());
		realizedStyle.addAttribute(Constants.StyleType,
					   Constants.STCharacter);
	    } else if (sectionStyle) {
		realizedStyle.addAttributes(currentSectionAttributes());
	        realizedStyle.addAttribute(Constants.StyleType,
					   Constants.STSection);
	    } else { /* must be a paragraph style */
		realizedStyle.addAttributes(currentParagraphAttributes());
	        realizedStyle.addAttribute(Constants.StyleType,
					   Constants.STParagraph);
	    }

	    if (nextStyle != STYLENUMBER_NONE) {
		StyleDefiningDestination styleDest;
		styleDest = (StyleDefiningDestination)definedStyles.get(Integer.valueOf(nextStyle));
		if (styleDest != null) {
		    next = styleDest.realize();
		}
	    }

	    if (next != null)
		realizedStyle.addAttribute(Constants.StyleNext, next);
	    realizedStyle.addAttribute(Constants.StyleAdditive,
				       Boolean.valueOf(additive));
	    realizedStyle.addAttribute(Constants.StyleHidden,
				       Boolean.valueOf(hidden));

	    return realizedStyle;
	}
    }
}

/** Handles the info group. Currently no info keywords are recognized
 *  so this is a subclass of DiscardingDestination. */
class InfoDestination
    extends DiscardingDestination
    implements Destination
{
}

/** RTFReader.TextHandlingDestination is an abstract RTF destination
 *  which simply tracks the attributes specified by the RTF control words
 *  in internal form and can produce acceptable AttributeSets for the
 *  current character, paragraph, and section attributes. It is up
 *  to the subclasses to determine what is done with the actual text. */
abstract class AttributeTrackingDestination implements Destination
{
    /** This is the "chr" element of parserState, cached for
     *  more efficient use */
    MutableAttributeSet characterAttributes;
    /** This is the "pgf" element of parserState, cached for
     *  more efficient use */
    MutableAttributeSet paragraphAttributes;
    /** This is the "sec" element of parserState, cached for
     *  more efficient use */
    MutableAttributeSet sectionAttributes;
    
    public AttributeTrackingDestination()
    {
	characterAttributes = rootCharacterAttributes();
	parserState.put("chr", characterAttributes);
	paragraphAttributes = rootParagraphAttributes();
	parserState.put("pgf", paragraphAttributes);
	sectionAttributes = rootSectionAttributes();
	parserState.put("sec", sectionAttributes);
    }

    abstract public void handleText(String text);

    public void handleBinaryBlob(byte[] data)
    {
        /* This should really be in TextHandlingDestination, but
	 * since *nobody* does anything with binary blobs, this
	 * is more convenient. */
	warning("Unexpected binary data in RTF file.");
    }

    public void begingroup()
    {
	AttributeSet characterParent = currentTextAttributes();
	AttributeSet paragraphParent = currentParagraphAttributes();
	AttributeSet sectionParent = currentSectionAttributes();

	/* It would probably be more efficient to use the 
	 * resolver property of the attributes set for
	 * implementing rtf groups,
	 * but that's needed for styles. */

	/* update the cached attribute dictionaries */
	characterAttributes = new SimpleAttributeSet();
	characterAttributes.addAttributes(characterParent);
	parserState.put("chr", characterAttributes);

	paragraphAttributes = new SimpleAttributeSet();
	paragraphAttributes.addAttributes(paragraphParent);
	parserState.put("pgf", paragraphAttributes);

	sectionAttributes = new SimpleAttributeSet();
	sectionAttributes.addAttributes(sectionParent);
	parserState.put("sec", sectionAttributes);
    }

    public void endgroup(Dictionary oldState)
    {
	characterAttributes = (MutableAttributeSet)parserState.get("chr");
	paragraphAttributes = (MutableAttributeSet)parserState.get("pgf");
	sectionAttributes   = (MutableAttributeSet)parserState.get("sec");
    }

    public void close()
    {
    }

    public boolean handleKeyword(String keyword)
    {
	if (keyword.equals("ulnone")) {
	    return handleKeyword("ul", 0);
	}
	
	{
	    Object item = straightforwardAttributes.get(keyword);
	    if (item != null) {
	        RTFAttribute attr = (RTFAttribute)item;
		boolean ok;
		
		switch(attr.domain()) {
		  case RTFAttribute.D_CHARACTER:
		    ok = attr.set(characterAttributes);
		    break;
		  case RTFAttribute.D_PARAGRAPH:
		    ok = attr.set(paragraphAttributes);
		    break;
		  case RTFAttribute.D_SECTION:
		    ok = attr.set(sectionAttributes);
		    break;
		  case RTFAttribute.D_META:
		    mockery.backing = parserState;
		    ok = attr.set(mockery);
		    mockery.backing = null;
		    break;
		  case RTFAttribute.D_DOCUMENT:
		    ok = attr.set(documentAttributes);
		    break;
		  default:
		    /* should never happen */
		    ok = false;
		    break;
		}
		if (ok)
		    return true;
	    }
	}

	
	if (keyword.equals("plain")) {
	    resetCharacterAttributes();
	    return true;
	}
	
	if (keyword.equals("pard")) {
	    resetParagraphAttributes();
	    return true;
	}

	if (keyword.equals("sectd")) {
	    resetSectionAttributes();
	    return true;
	}

	return false;
    }

    public boolean handleKeyword(String keyword, int parameter)
    {
	boolean booleanParameter = (parameter != 0);
	
	if (keyword.equals("fc"))
	    keyword = "cf"; /* whatEVER, dude. */
	
	if (keyword.equals("f")) {
	    parserState.put(keyword, Integer.valueOf(parameter));
	    return true;
	}
	if (keyword.equals("cf")) {
	    parserState.put(keyword, Integer.valueOf(parameter));
	    return true;
	}

	{
	    Object item = straightforwardAttributes.get(keyword);
	    if (item != null) {
	        RTFAttribute attr = (RTFAttribute)item;
		boolean ok;
		
		switch(attr.domain()) {
		  case RTFAttribute.D_CHARACTER:
		    ok = attr.set(characterAttributes, parameter);
		    break;
		  case RTFAttribute.D_PARAGRAPH:
		    ok = attr.set(paragraphAttributes, parameter);
		    break;
		  case RTFAttribute.D_SECTION:
		    ok = attr.set(sectionAttributes, parameter);
		    break;
		  case RTFAttribute.D_META:
		    mockery.backing = parserState;
		    ok = attr.set(mockery, parameter);
		    mockery.backing = null;
		    break;
		  case RTFAttribute.D_DOCUMENT:
		    ok = attr.set(documentAttributes, parameter);
		    break;
		  default:
		    /* should never happen */
		    ok = false;
		    break;
		}
		if (ok)
		    return true;
	    }
	}

	if (keyword.equals("fs")) {
	    StyleConstants.setFontSize(characterAttributes, (parameter / 2));
	    return true;
	}

	/* TODO: superscript/subscript */
	
	if (keyword.equals("sl")) {
	    if (parameter == 1000) {  /* magic value! */
		characterAttributes.removeAttribute(StyleConstants.LineSpacing);
	    } else {
		/* TODO: The RTF sl attribute has special meaning if it's
		   negative. Make sure that SwingText has the same special
		   meaning, or find a way to imitate that. When SwingText
		   handles this, also recognize the slmult keyword. */
		StyleConstants.setLineSpacing(characterAttributes,
					      parameter / 20f);
	    }
	    return true;
	}
	
	/* TODO: Other kinds of underlining */
	
	if (keyword.equals("tx") || keyword.equals("tb")) {
	    float tabPosition = parameter / 20f;
	    int tabAlignment, tabLeader;
	    Number item;
	    
	    tabAlignment = TabStop.ALIGN_LEFT;
	    item = (Number)(parserState.get("tab_alignment"));
	    if (item != null)
		tabAlignment = item.intValue();
	    tabLeader = TabStop.LEAD_NONE;
	    item = (Number)(parserState.get("tab_leader"));
	    if (item != null)
		tabLeader = item.intValue();
	    if (keyword.equals("tb"))
		tabAlignment = TabStop.ALIGN_BAR;
	    
	    parserState.remove("tab_alignment");
	    parserState.remove("tab_leader");
	    
	    TabStop newStop = new TabStop(tabPosition, tabAlignment, tabLeader);
	    Dictionary tabs;
	    Integer stopCount;
	    
	    tabs = (Dictionary)parserState.get("_tabs");
	    if (tabs == null) {
		tabs = new Hashtable();
		parserState.put("_tabs", tabs);
		stopCount = Integer.valueOf(1);
	    } else {
		stopCount = (Integer)tabs.get("stop count");
		stopCount = Integer.valueOf(1 + stopCount.intValue());
	    }
	    tabs.put(stopCount, newStop);
	    tabs.put("stop count", stopCount);
	    parserState.remove("_tabs_immutable");
	    
	    return true;
	}

	if (keyword.equals("s") &&
	    paragraphStyles != null) {
	    parserState.put("paragraphStyle", paragraphStyles[parameter]);
	    return true;
	}

	if (keyword.equals("cs") &&
	    characterStyles != null) {
	    parserState.put("characterStyle", characterStyles[parameter]);
	    return true;
	}

	if (keyword.equals("ds") &&
	    sectionStyles != null) {
	    parserState.put("sectionStyle", sectionStyles[parameter]);
	    return true;
	}

	return false;
    }

    /** Returns a new MutableAttributeSet containing the
     *  default character attributes */
    protected MutableAttributeSet rootCharacterAttributes()
    {
	MutableAttributeSet set = new SimpleAttributeSet();
	
	/* TODO: default font */
	
	StyleConstants.setItalic(set, false);
	StyleConstants.setBold(set, false);
	StyleConstants.setUnderline(set, false);
	StyleConstants.setForeground(set, defaultColor());

	return set;
    }

    /** Returns a new MutableAttributeSet containing the
     *  default paragraph attributes */
    protected MutableAttributeSet rootParagraphAttributes()
    {
	MutableAttributeSet set = new SimpleAttributeSet();
	
	StyleConstants.setLeftIndent(set, 0f);
	StyleConstants.setRightIndent(set, 0f);
	StyleConstants.setFirstLineIndent(set, 0f);
	
	/* TODO: what should this be, really? */
	set.setResolveParent(target.getStyle(StyleContext.DEFAULT_STYLE));
	
	return set;
    }

    /** Returns a new MutableAttributeSet containing the
     *  default section attributes */
    protected MutableAttributeSet rootSectionAttributes()
    {
        MutableAttributeSet set = new SimpleAttributeSet();

	return set;
    }

    /**
     * Calculates the current text (character) attributes in a form suitable
     * for SwingText from the current parser state.
     *
     * @returns a new MutableAttributeSet containing the text attributes.
     */
    MutableAttributeSet currentTextAttributes()
    {
	MutableAttributeSet attributes =
	    new SimpleAttributeSet(characterAttributes);
	Integer fontnum;
	Integer stateItem;

	/* figure out the font name */
	/* TODO: catch exceptions for undefined attributes,
	   bad font indices, etc.? (as it stands, it is the caller's
	   job to clean up after corrupt RTF) */
	fontnum = (Integer)parserState.get("f");
	/* note setFontFamily() can not handle a null font */
	String fontFamily;
	if (fontnum != null)
	    fontFamily = (String)fontTable.get(fontnum);
	else
	    fontFamily = null;
	if (fontFamily != null)
	    StyleConstants.setFontFamily(attributes, fontFamily);
	else
	    attributes.removeAttribute(StyleConstants.FontFamily);
    
	if (colorTable != null) {
	    stateItem = (Integer)parserState.get("cf");
	    if (stateItem != null) {
		Color fg = colorTable[stateItem.intValue()];
		StyleConstants.setForeground(attributes, fg);
	    } else {
		/* AttributeSet dies if you set a value to null */
		attributes.removeAttribute(StyleConstants.Foreground);
	    }
	}
	
	if (colorTable != null) {
	    stateItem = (Integer)parserState.get("cb");
	    if (stateItem != null) {
		Color bg = colorTable[stateItem.intValue()];
		attributes.addAttribute(StyleConstants.Background,
					bg);
	    } else {
		/* AttributeSet dies if you set a value to null */
		attributes.removeAttribute(StyleConstants.Background);
	    }
	}
	
	Style characterStyle = (Style)parserState.get("characterStyle");
	if (characterStyle != null)
	    attributes.setResolveParent(characterStyle);

	/* Other attributes are maintained directly in "attributes" */
	
	return attributes;
    }

    /**
     * Calculates the current paragraph attributes (with keys
     * as given in StyleConstants) from the current parser state.
     *
     * @returns a newly created MutableAttributeSet. 
     * @see StyleConstants
     */
    MutableAttributeSet currentParagraphAttributes()
    {
	/* NB if there were a mutableCopy() method we should use it */
	MutableAttributeSet bld = new SimpleAttributeSet(paragraphAttributes);
	
	Integer stateItem;
	
	/*** Tab stops ***/
	TabStop tabs[];
	
	tabs = (TabStop[])parserState.get("_tabs_immutable");
	if (tabs == null) {
	    Dictionary workingTabs = (Dictionary)parserState.get("_tabs");
	    if (workingTabs != null) {
		int count = ((Integer)workingTabs.get("stop count")).intValue();
		tabs = new TabStop[count];
		for (int ix = 1; ix <= count; ix ++)
		    tabs[ix-1] = (TabStop)workingTabs.get(Integer.valueOf(ix));
		parserState.put("_tabs_immutable", tabs);
	    }
	}
	if (tabs != null)
	    bld.addAttribute(Constants.Tabs, tabs);

	Style paragraphStyle = (Style)parserState.get("paragraphStyle");
	if (paragraphStyle != null)
	    bld.setResolveParent(paragraphStyle);
	
	return bld;
    }
    
    /**
     * Calculates the current section attributes
     * from the current parser state.
     *
     * @returns a newly created MutableAttributeSet. 
     */
    public AttributeSet currentSectionAttributes()
    {
	MutableAttributeSet attributes = new SimpleAttributeSet(sectionAttributes);

	Style sectionStyle = (Style)parserState.get("sectionStyle");
	if (sectionStyle != null)
	    attributes.setResolveParent(sectionStyle);

	return attributes;
    }

    /** Resets the filter's internal notion of the current character
     *  attributes to their default values. Invoked to handle the
     *  \plain keyword. */
    protected void resetCharacterAttributes()
    {
	handleKeyword("f", 0);
	handleKeyword("cf", 0);
	
	handleKeyword("fs", 24);  /* 12 pt. */
	
	Enumeration attributes = straightforwardAttributes.elements();
	while(attributes.hasMoreElements()) {
	    RTFAttribute attr = (RTFAttribute)attributes.nextElement();
	    if (attr.domain() == RTFAttribute.D_CHARACTER) 
	        attr.setDefault(characterAttributes);
	}
	
	handleKeyword("sl", 1000);

	parserState.remove("characterStyle");
    }

    /** Resets the filter's internal notion of the current paragraph's
     *  attributes to their default values. Invoked to handle the
     *  \pard keyword. */
    protected void resetParagraphAttributes()
    {
	parserState.remove("_tabs");
	parserState.remove("_tabs_immutable");
	parserState.remove("paragraphStyle");
	
	StyleConstants.setAlignment(paragraphAttributes, 
				    StyleConstants.ALIGN_LEFT);

	Enumeration attributes = straightforwardAttributes.elements();
	while(attributes.hasMoreElements()) {
	    RTFAttribute attr = (RTFAttribute)attributes.nextElement();
	    if (attr.domain() == RTFAttribute.D_PARAGRAPH) 
	        attr.setDefault(characterAttributes);
	}
    }
    
    /** Resets the filter's internal notion of the current section's
     *  attributes to their default values. Invoked to handle the
     *  \sectd keyword. */
    protected void resetSectionAttributes()
    {
	Enumeration attributes = straightforwardAttributes.elements();
	while(attributes.hasMoreElements()) {
	    RTFAttribute attr = (RTFAttribute)attributes.nextElement();
	    if (attr.domain() == RTFAttribute.D_SECTION) 
	        attr.setDefault(characterAttributes);
	}

	parserState.remove("sectionStyle");
    }
}

/** RTFReader.TextHandlingDestination provides basic text handling
 *  functionality. Subclasses must implement: <dl>
 *  <dt>deliverText()<dd>to handle a run of text with the same
 *                       attributes
 *  <dt>finishParagraph()<dd>to end the current paragraph and
 *                           set the paragraph's attributes
 *  <dt>endSection()<dd>to end the current section
 *  </dl>
 */
abstract class TextHandlingDestination
    extends AttributeTrackingDestination
    implements Destination
{
    /** <code>true</code> if the reader has not just finished
     *  a paragraph; false upon startup */
    boolean inParagraph;
    
    public TextHandlingDestination()
    {
        super();
	inParagraph = false;
    }

    public void handleText(String text)
    {
	if (! inParagraph)
	    beginParagraph();
	
	deliverText(text, currentTextAttributes());
    }

    abstract void deliverText(String text, AttributeSet characterAttributes);

    public void close()
    {
	if (inParagraph)
	    endParagraph();

	super.close();
    }

    public boolean handleKeyword(String keyword)
    {
	if (keyword.equals("\r") || keyword.equals("\n")) {
	    keyword = "par";
	}
	
	if (keyword.equals("par")) {
//	    warnings.println("Ending paragraph.");
	    endParagraph();
	    return true;
	}

	if (keyword.equals("sect")) {
//	    warnings.println("Ending section.");
	    endSection();
	    return true;
	}
	
	return super.handleKeyword(keyword);
    }

    protected void beginParagraph()
    {
	inParagraph = true;
    }
    
    protected void endParagraph()
    {
	AttributeSet pgfAttributes = currentParagraphAttributes();
	AttributeSet chrAttributes = currentTextAttributes();
	finishParagraph(pgfAttributes, chrAttributes);
	inParagraph = false;
    }

    abstract void finishParagraph(AttributeSet pgfA, AttributeSet chrA);

    abstract void endSection();
}

/** RTFReader.DocumentDestination is a concrete subclass of
 *  TextHandlingDestination which appends the text to the
 *  StyledDocument given by the <code>target</code> ivar of the
 *  containing RTFReader.
 */
class DocumentDestination
    extends TextHandlingDestination
    implements Destination
{
    public void deliverText(String text, AttributeSet characterAttributes)
    {
	try {
	    target.insertString(target.getLength(),
				text,
				currentTextAttributes());
	} catch (BadLocationException ble) {
	    /* This shouldn't be able to happen, of course */
	    /* TODO is InternalError the correct error to throw? */
	    throw new InternalError(ble.getMessage());
	}
    }

    public void finishParagraph(AttributeSet pgfAttributes,
				AttributeSet chrAttributes)
    {
	int pgfEndPosition = target.getLength();
	try {
	    target.insertString(pgfEndPosition, "\n", chrAttributes);
	    target.setParagraphAttributes(pgfEndPosition, 1, pgfAttributes, true);
	} catch (BadLocationException ble) {
	    /* This shouldn't be able to happen, of course */
	    /* TODO is InternalError the correct error to throw? */
	    throw new InternalError(ble.getMessage());
	}
    }

    public void endSection()
    {
        /* If we implemented sections, we'd end 'em here */
    }
}    

}


